package com.linfeng.server.oauth.config;

import com.linfeng.component.tools.RedisTools;
import com.linfeng.server.oauth.data.AdminDetails;
import com.linfeng.server.oauth.exception.CustomWebResponseExceptionTranslator;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@EnableAuthorizationServer
@Configuration
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private AuthenticationManager manager;
    @Resource
    UserDetailsService userDetailsService;

    /**
     * 令牌持久化配置
     */
    private TokenStore tokenStore;

    /**
     * jwtToken解析器
     */
    private final JwtAccessTokenConverter jwtAccessTokenConverter;

    /**
     * 授权码服务
     */
    private final AuthorizationCodeServices authorizationCodeServices;

    private final PasswordEncoder passwordEncoder;

    @Resource
    private JwtProperties jwtProperties;

    @Resource
    RedisTools redisTools;

    /**
     * 这个方法是对客户端进行配置，一个验证服务器可以预设很多个客户端，
     * 之后这些指定的客户端就可以按照下面指定的方式进行验证
     *
     * @param clients 客户端配置工具
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients
                //这里我们直接硬编码创建，当然也可以像Security那样自定义或是使用JDBC从数据库读取
                .inMemory()
                //客户端名称，随便起就行
                .withClient("web")
                //只与客户端分享的secret，随便写，但是注意要加密
                .secret(passwordEncoder.encode("654321"))
                //自动审批，这里关闭，要的就是一会体验那种感觉
//                .autoApprove(false)
                //授权范围，这里我们使用全部all
                .scopes("book", "user", "borrow")
                //可以写多个，当有多个时需要在验证请求中指定使用哪个地址进行回调
                .redirectUris("http://localhost:8084/code")
                //授权模式，一共支持5种，除了之前我们介绍的四种之外，还有一个刷新Token的模式
                //这里我们直接把五种都写上，方便一会实验，当然各位也可以单独只写一种一个一个进行测试
                //现在我们指定的客户端就支持这五种类型的授权方式了
                .authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                // 编码器设定为BCryptPasswordEncoder
                .passwordEncoder(passwordEncoder)
                //允许客户端使用表单验证，一会我们POST请求中会携带表单信息
                .allowFormAuthenticationForClients()
                // oauth/token_key 公开密钥
                .tokenKeyAccess("permitAll()")
                //允许所有的Token查询请求
                .checkTokenAccess("permitAll()");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                // 认证管理器
                .authenticationManager(manager)
                //由于SpringSecurity新版本的一些底层改动，这里需要配置一下authenticationManager，才能正常使用password模式
                .userDetailsService(userDetailsService)
                // 授权码服务
                .authorizationCodeServices(authorizationCodeServices)
                // 令牌管理服务
                .tokenServices(tokenServices())
                // 自定义异常
                .exceptionTranslator(new CustomWebResponseExceptionTranslator())
        ;
    }

    /**
     * 令牌服务配置
     *
     * @return 令牌服务对象
     */
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        tokenServices.setSupportRefreshToken(true);

        // 令牌增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();

        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtAccessTokenConverter);
        delegates.add(tokenEnhancer());

        tokenEnhancerChain.setTokenEnhancers(delegates);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);
        // 令牌默认有效期7天
        tokenServices.setAccessTokenValiditySeconds(jwtProperties.TOKEN_ACCESS_EXPIRES);
        // 刷新令牌默认有效期8天
        tokenServices.setRefreshTokenValiditySeconds(jwtProperties.TOKEN_REFRESH_EXPIRES);
        return tokenServices;
    }

    /**
     * token自定义内容
     *
     * @return
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {

        return (oAuth2AccessToken, oAuth2Authentication) -> {

            Object principal = oAuth2Authentication.getPrincipal();
            if (principal instanceof AdminDetails) {
                AdminDetails adminDetails = (AdminDetails) principal;
                Map<String, Object> info = new HashMap<>();
                info.put("adminId", ((AdminDetails) principal).getAdminId());
                redisTools.set(adminDetails.getUsername(), adminDetails.getAdminId(), jwtProperties.TOKEN_ACCESS_EXPIRES);
                log.info("redis存储adminId-----------------");
                ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
            }
            return oAuth2AccessToken;
        };
    }
}
